//
//  GStreamerBackend.m
//  GStreamerTutorial2
//
//  Created by 冯思 on 15/12/1.
//  Copyright © 2015年 fengsi. All rights reserved.
//

#import <Foundation/Foundation.h>
#import "GStreamerBackend.h"

#include <gst/gst.h>
#include <gst/video/videooverlay.h>
#include <gst/rtsp/gstrtsp.h>
#include <gst/app/app.h>

GST_DEBUG_CATEGORY_STATIC(debug_category);
#define GST_CAT_DEFAULT debug_category

@interface GStreamerBackend()
- (void)setUIMessage: (gchar*)message;
- (void)app_function;
- (void)check_initialization_complete;
@end

@implementation GStreamerBackend {
    id ui_delegate;         /* Class that we use to interact with the user interface */
    GstElement *pipeline;   /* The running pipeline */
    GMainContext *context;  /* GLib context used to run the main loop */
    GMainLoop *main_loop;   /* GLib main loop */
    gboolean initialized;   /* To avoid informing the UI multiple times about the initialization */
    gboolean need_search_for_sps_pps;
    NSData *ppsData, *spsData;
    CMVideoFormatDescriptionRef videoFormatDescription;
    AVSampleBufferDisplayLayer *ui_video_view; /* layer that holds the video */
}

/*
 * Interface methods
 */

- (NSString *)getGStreamerVersion
{
    char *version_utf8 = gst_version_string();
    NSString *version_string = [NSString stringWithUTF8String:version_utf8];
    g_free(version_utf8);
    return version_string;
}

- (id) init:(id) uiDelegate videoView: (AVSampleBufferDisplayLayer *)video_view {
    if (self = [super init]) {
        self->ui_delegate = uiDelegate;
        self->ui_video_view = video_view;
        
        need_search_for_sps_pps = true;
        ppsData = nil;
        spsData = nil;
        
        GST_DEBUG_CATEGORY_INIT(debug_category, "tutorial-2", 0, "iOS tutorial 2");
        gst_debug_set_threshold_for_name("tutorial-2", GST_LEVEL_DEBUG);
        
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [self app_function];
        });
    }
    
    return self;
}

-(void) dealloc {
    if (pipeline) {
        GST_DEBUG("Setting the pipeline to NULL");
        gst_element_set_state(pipeline, GST_STATE_NULL);
        gst_object_unref(pipeline);
        pipeline = NULL;
    }
}

-(void) play {
    if (gst_element_set_state(pipeline, GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) {
        [self setUIMessage:"Failed to set pipeline to playing"];
    }
}

-(void) pause {
    if (gst_element_set_state(pipeline, GST_STATE_PAUSED) == GST_STATE_CHANGE_FAILURE) {
        [self setUIMessage:"Failed to set pipeline to pause"];
    }
}

/*
 * Private methods
 */

-(void) setUIMessage:(gchar *)message {
    NSString *string = [NSString stringWithUTF8String:message];
    if (ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerSetUIMessage:)]) {
        [ui_delegate gstreamerSetUIMessage:string];
    }
}

static void error_cb(GstBus *bus, GstMessage *msg, GStreamerBackend *self) {
    GError *err;
    gchar *debug_info;
    gchar *message_string;
    
    gst_message_parse_error(msg, &err, &debug_info);
    message_string = g_strdup_printf("Error received from element %s: %s", GST_OBJECT_NAME(msg->src), err->message);
    GST_DEBUG("%s", debug_info);
    g_clear_error(&err);
    g_free(debug_info);
    [self setUIMessage:message_string];
    g_free(message_string);
    gst_element_set_state(self->pipeline, GST_STATE_NULL);
    NSString *st = nil;
}

static void state_changed_cb(GstBus *bus, GstMessage *msg, GStreamerBackend *self) {
    GstState old_state, new_state, pending_state;
    gst_message_parse_state_changed(msg, &old_state, &new_state, &pending_state);
    if (GST_MESSAGE_SRC(msg) == GST_OBJECT(self->pipeline)) {
        gchar *message = g_strdup_printf("State changed to %s", gst_element_state_get_name(new_state));
        [self setUIMessage:message];
        g_free(message);
    }
}

static void on_pad_added(GstElement *element, GstPad *pad, gpointer data) {
    GstElement *capsfilter = (GstElement *) data;
    GstPad *sinkpad = gst_element_get_static_pad(capsfilter, "sink");
    
    if (gst_pad_link(pad, sinkpad) == GST_PAD_LINK_OK) {
        GST_DEBUG("link ok!");
    } else {
        GST_DEBUG("link wrong!");
    }
//    g_print("%s\n", gst_pad_get_name(pad));
//    GST_DEBUG("rtspsrc -> capsfilter");
    
//    GstCaps *srcpad_caps = gst_pad_get_pad_template_caps(pad);
//    GST_DEBUG("%s\n\n", gst_caps_to_string(srcpad_caps));
//    gst_caps_unref(srcpad_caps);
    
    gst_object_unref(sinkpad);
}

-(void) check_initialization_complete {
    if (!initialized && main_loop) {
        GST_DEBUG("Initialization complete, notifying application.");
        if (ui_delegate && [ui_delegate respondsToSelector:@selector(gstreamerInitialized)]) {
            [ui_delegate gstreamerInitialized];
        }
        initialized = TRUE;
    }
}

//static void dump_sample_data(GstMapInfo info) {
//    g_print("****** Start of a sample ******\n");
//    for (int i = 0; i < info.size; i++) {
//        g_print("%02X ", (uint8_t)info.data[i]);
//    }
//    g_print("\n****** End of a sample ******\n\n");
//}

static GstFlowReturn new_sample(GstAppSink *sink, gpointer user_data) {
    GStreamerBackend *self = (__bridge GStreamerBackend *)user_data;
    GstSample *sample = gst_app_sink_pull_sample(sink);
    GstBuffer *buffer = gst_sample_get_buffer(sample);
    GstMemory *memory = gst_buffer_get_all_memory(buffer);
    
    GstMapInfo info;
    gst_memory_map(memory, &info, GST_MAP_READ);
    
    int startCodeIndex = 0;
    for (int i = 0; i < 5; i++) {
        if (info.data[i] == 0x01) {
            startCodeIndex = i;
            break;
        }
    }
    int nalu_type = ((uint8_t)info.data[startCodeIndex + 1] & 0x1f);
    
    if (self->need_search_for_sps_pps) {
        if (nalu_type == 7) {
            self->spsData = [NSData dataWithBytes:&info.data[startCodeIndex + 1] length:info.size - 4];
        }
        if (nalu_type == 8) {
            self->ppsData = [NSData dataWithBytes:&info.data[startCodeIndex + 1] length:info.size - 4];
        }
        
        if (self->spsData != nil && self->ppsData != nil) {
            const uint8_t* const parameterSetPointers[2] = { (const uint8_t *)[self->spsData bytes], (const uint8_t *)[self->ppsData bytes] };
            const size_t parameterSetSizes[2] = { [self->spsData length], [self->ppsData length] };
            
//            CMVideoFormatDescriptionRef videoFormatDescription;
            OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
                                                                                  2,
                                                                                  parameterSetPointers,
                                                                                  parameterSetSizes,
                                                                                  4,
                                                                                  &self->videoFormatDescription);
            self->need_search_for_sps_pps = false;
            g_print("Found all data for CMVideoFormatDescription. Creation: %s\n", (status == noErr) ? "successfully." : "failed");
        }
    } else if (nalu_type == 1 || nalu_type == 5) {
        CMBlockBufferRef videoBlock = NULL;
        OSStatus status;
        
        status = CMBlockBufferCreateWithMemoryBlock(NULL, info.data, info.size, kCFAllocatorNull, NULL, 0, info.size, 0, &videoBlock);
//        g_print("BlockBufferCreation: %s\n", (status == kCMBlockBufferNoErr) ? "successfully." : "failed");
        
        uint64_t sampleLength = info.size - 4;
        const uint8_t sourceBytes[] = { (uint8_t)(sampleLength >> 24), (uint8_t)(sampleLength >> 16), (uint8_t)(sampleLength >> 8), (uint8_t)sampleLength };
        status = CMBlockBufferReplaceDataBytes(sourceBytes, videoBlock, 0, 4);
//        g_print("BlockBufferReplace: %s\n", (status == kCMBlockBufferNoErr) ? "successfully." : "failed");
        
        CMSampleBufferRef sampleBuffer = NULL;
        const size_t sampleSizeArray[] = {info.size};
        status = CMSampleBufferCreate(kCFAllocatorDefault,
                                      videoBlock, true, NULL, NULL, self->videoFormatDescription, 1, 0, NULL, 1, sampleSizeArray, &sampleBuffer);
//        g_print("SampleBufferCreate: %s\n", (status == noErr) ? "successfully." : "failed");
        
        CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
        CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
        CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
        
        dispatch_sync(dispatch_get_main_queue(), ^{
            [self->ui_video_view enqueueSampleBuffer:sampleBuffer];
            [self->ui_video_view setNeedsDisplay];
        });
    }
    
    gst_memory_unmap(memory, &info);
    gst_memory_unref(memory);
    gst_buffer_unref(buffer);
    
    return GST_FLOW_OK;
}

-(void) app_function {
    GstElement *rtspsrc, *capsfilter1, *rtph264depay, *capsfilter2, *appsink;
    GstBus *bus;
    GSource *bus_source;
    
    context = g_main_context_new();
    g_main_context_push_thread_default(context);
    
    // BEGIN --- Building pipeline ---
    
    GST_DEBUG("Creating pipeline");
    
    pipeline = gst_pipeline_new("rtspsrc2appsink");
    
    rtspsrc = gst_element_factory_make("rtspsrc", "rtspsrc");
    capsfilter1 = gst_element_factory_make("capsfilter", "capsfilter1");
    rtph264depay = gst_element_factory_make("rtph264depay", "rtph264depay");
    capsfilter2 = gst_element_factory_make("capsfilter", "capsfilter2");
    appsink = gst_element_factory_make("appsink", "appsink");
    
    g_object_set(rtspsrc,
                 "location", "rtsp://192.168.1.1:8557/2?videoCodecType=H.264",
                 "protocols", GST_RTSP_LOWER_TRANS_TCP,
//                 "debug", true,
                 nil);
    
    GstCaps *caps = gst_caps_new_simple("application/x-rtp",
                                        "media", G_TYPE_STRING, "video",
                                        "clock-rate", G_TYPE_INT, 90000,
                                        "encoding-name", G_TYPE_STRING, "H264",
                                        nil);
    g_object_set(capsfilter1, "caps", caps, nil);
    gst_caps_unref(caps);
    
    caps = gst_caps_new_simple("video/x-h264",
                               "streamformat", G_TYPE_STRING, "byte-stream",
                               "alignment", G_TYPE_STRING, "nal",
                               nil);
    g_object_set(capsfilter2, "caps", caps, nil);
    gst_caps_unref(caps);
    
    gst_bin_add_many(GST_BIN(pipeline), rtspsrc, capsfilter1, rtph264depay, capsfilter2, appsink, nil);
    
    if (!gst_element_link_many(capsfilter1, rtph264depay, capsfilter2, appsink, nil)) {
        GST_DEBUG("Cannot link gstreamer elements!");
        exit(1);
    }
    g_signal_connect(rtspsrc, "pad-added", G_CALLBACK(on_pad_added), capsfilter1);
    
    GstAppSinkCallbacks callbacks = { NULL, NULL, new_sample, NULL, NULL };
    gst_app_sink_set_callbacks(GST_APP_SINK(appsink), &callbacks, (__bridge gpointer)self, NULL);
    
    if (gst_element_set_state(pipeline, GST_STATE_READY) != GST_STATE_CHANGE_SUCCESS) {
        GST_DEBUG("Cannot change state to READY!");
    }
    
    // END --- Building pipeline ---
    
    bus = gst_element_get_bus(pipeline);
    bus_source = gst_bus_create_watch(bus);
    g_source_set_callback(bus_source, (GSourceFunc)gst_bus_async_signal_func, NULL, NULL);
    g_source_attach(bus_source, context);
    g_source_unref(bus_source);
    g_signal_connect(G_OBJECT(bus), "message::error", (GCallback)error_cb, (__bridge void *)self);
    g_signal_connect(G_OBJECT(bus), "message::state-changed", (GCallback)state_changed_cb, (__bridge void *)self);
    gst_object_unref(bus);
    
    GST_DEBUG("Entering main loop...");
    main_loop = g_main_loop_new(context, FALSE);
    [self check_initialization_complete];
    g_main_loop_run(main_loop);
    GST_DEBUG("Exited main loop");
    g_main_loop_unref(main_loop);
    main_loop = NULL;
    
    g_main_context_pop_thread_default(context);
    g_main_context_unref(context);
    gst_element_set_state(pipeline, GST_STATE_NULL);
    gst_object_unref(pipeline);
    
    return;
}

@end